{
  "nbformat": 4,
  "nbformat_minor": 0,
  "metadata": {
    "colab": {
      "name": "RNN_PT.ipynb",
      "provenance": []
    },
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    }
  },
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "BVq5mz9s8CPL",
        "colab_type": "text"
      },
      "source": [
        "# Building RNNs is Fun with PyTorch and Google Colab\n",
        "\n",
        "> \"This notebook teaches you how to build a recurrent neural network (RNN) with a single layer, consisting of one single neuron. It also teaches how to implement a simple RNN-based model for image classification.\"\n",
        "\n",
        "- toc: false\n",
        "- branch: master\n",
        "- author: Elvis Saravia\n",
        "- badges: true\n",
        "- comments: true\n",
        "- categories: [neural network, beginner, pytorch, rnn]\n",
        "- image: images/rnn-pt.png\n",
        "- hide: false"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "mL0IAaveyWJn",
        "colab_type": "text"
      },
      "source": [
        "## About\n",
        "In this tutorial, I will first teach you how to build a recurrent neural network (RNN) with a single layer, consisting of one single neuron, with PyTorch and Google Colab. I will also show you how to implement a simple RNN-based model for image classification.\n",
        "\n",
        "This work is heavily inspired by Aurélien Géron's book called [\"Hand-On Machine Learning with Scikit-Learn and TensorFlow\"](https://www.oreilly.com/library/view/hands-on-machine-learning/9781491962282/). Although his neural network implementations are purely in TensorFlow, I adopted/reused some notations/variables names and implemented things using PyTorch only. I really enjoyed his book and learned a lot from his explanations. His work inspired this tutorial and I strongly recommend the book."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "mXs_oWct-qxY",
        "colab_type": "text"
      },
      "source": [
        "We first import the necessary libraries we will use in the tutorial:"
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "BBuAr4-L8CPN",
        "colab_type": "code",
        "colab": {}
      },
      "source": [
        "import torch\n",
        "import torch.nn as nn\n",
        "import torch.nn.functional as F\n",
        "import os\n",
        "import numpy as np"
      ],
      "execution_count": 0,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "EMQfRrRl8CPV",
        "colab_type": "text"
      },
      "source": [
        "### RNN with A Single Neuron\n",
        "\n",
        "\n",
        "The idea of this tutorial is to show you the basic operations necessary for building an RNN architecture using PyTorch. This guide assumes you have knowledge of basic RNNs and that you have read the tutorial on [building neural networks from scratch using PyTorch](https://medium.com/dair-ai/a-simple-neural-network-from-scratch-with-pytorch-and-google-colab-c7f3830618e0). I will try to review RNNs wherever possible for those that need a refresher but I will keep it minimal.\n",
        "\n",
        "First, let's build the computation graph for a single-layer RNN. Again, we are not concerned with the math for now, I just want to show you the PyTorch operations needed to build your RNN models.\n",
        "\n",
        "For illustration purposes, this is the architecture we are building:\n",
        "\n",
        "![alt txt](https://docs.google.com/drawings/d/e/2PACX-1vQXBLYvvI1dqAHdLA0hQdsP1PojmCfuSCMK2DXEL0uTvRUqvD1eYK8fsECcNCoekxCbgWJ-k7QF_1s4/pub?w=600&h=400)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "kHjE4j2x8CPW",
        "colab_type": "text"
      },
      "source": [
        "And here is the code:"
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "qiY3EllI8CPY",
        "colab_type": "code",
        "colab": {}
      },
      "source": [
        "class SingleRNN(nn.Module):\n",
        "    def __init__(self, n_inputs, n_neurons):\n",
        "        super(SingleRNN, self).__init__()\n",
        "        \n",
        "        self.Wx = torch.randn(n_inputs, n_neurons) # 4 X 1\n",
        "        self.Wy = torch.randn(n_neurons, n_neurons) # 1 X 1\n",
        "        \n",
        "        self.b = torch.zeros(1, n_neurons) # 1 X 4\n",
        "        \n",
        "    def forward(self, X0, X1):\n",
        "        self.Y0 = torch.tanh(torch.mm(X0, self.Wx) + self.b) # 4 X 1\n",
        "        \n",
        "        self.Y1 = torch.tanh(torch.mm(self.Y0, self.Wy) +\n",
        "                            torch.mm(X1, self.Wx) + self.b) # 4 X 1\n",
        "        \n",
        "        return self.Y0, self.Y1"
      ],
      "execution_count": 0,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "cP94VBHE8CPc",
        "colab_type": "text"
      },
      "source": [
        "In the above code, I have implemented a simple one layer, one neuron RNN. I initialized two weight matrices, `Wx` and `Wy` with values from a normal distribution. `Wx` contains connection weights for the inputs of the current time step, while `Wy` contains connection weights for the outputs of the previous time step. We added a bias `b`. The `forward` function computes two outputs -- one for each time step... two in this case. Note that we are using `tanh` as the nonlinearity (activation function).\n",
        "\n",
        "As for the input, we are providing 4 instances, with each instance containing two input sequences.\n",
        "\n",
        "For illustration purposes, this is how the data is being fed into the RNN model: \n",
        "\n",
        "![alt txt](https://docs.google.com/drawings/d/e/2PACX-1vRpQYtOzO1U_3yQLf1885kMaja6MsXtJ8QnlqxrfpTgZmb4WpewJXphGdmotYXDB1VE6zlW6cBY_WqR/pub?w=600&h=600)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "ydSxau_eFpwH",
        "colab_type": "text"
      },
      "source": [
        "And this is the code to test the model:"
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "7a7-kIhj8CPe",
        "colab_type": "code",
        "colab": {}
      },
      "source": [
        "N_INPUT = 4\n",
        "N_NEURONS = 1\n",
        "\n",
        "X0_batch = torch.tensor([[0,1,2,0], [3,4,5,0], \n",
        "                         [6,7,8,0], [9,0,1,0]],\n",
        "                        dtype = torch.float) #t=0 => 4 X 4\n",
        "\n",
        "X1_batch = torch.tensor([[9,8,7,0], [0,0,0,0], \n",
        "                         [6,5,4,0], [3,2,1,0]],\n",
        "                        dtype = torch.float) #t=1 => 4 X 4\n",
        "\n",
        "model = SingleRNN(N_INPUT, N_NEURONS)\n",
        "\n",
        "Y0_val, Y1_val = model(X0_batch, X1_batch)"
      ],
      "execution_count": 0,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "uNaFkJUP8CPj",
        "colab_type": "text"
      },
      "source": [
        "After we have fed the input into the computation graph, we obtain outputs for each timestep (`Y0`, `Y1`), which we can now print as follows:"
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "q-teLNoV8CPl",
        "colab_type": "code",
        "outputId": "dd2652c6-240a-4168-b11a-bf8090f57b8e",
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 153
        }
      },
      "source": [
        "print(Y0_val)\n",
        "print(Y1_val)"
      ],
      "execution_count": 0,
      "outputs": [
        {
          "output_type": "stream",
          "text": [
            "tensor([[-0.1643],\n",
            "        [-0.9995],\n",
            "        [-1.0000],\n",
            "        [-1.0000]])\n",
            "tensor([[-1.0000],\n",
            "        [-0.6354],\n",
            "        [-1.0000],\n",
            "        [-0.9998]])\n"
          ],
          "name": "stdout"
        }
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "n6tcX6ii8CP0",
        "colab_type": "text"
      },
      "source": [
        "### Increasing Neurons in RNN Layer\n",
        "Next, I will show you how to generalize the RNN we have just build to let the single layer support an `n` amount of neurons. In terms of the architecture, nothing really changes since we have already parameterized the number of neurons in the computation graph we have built. However, the size of the output changes since we have changed the size of number of units (i.e., neurons) in the RNN layer. \n",
        "\n",
        "Here is an illustration of what we will build:\n",
        "\n",
        "![alt txt](https://docs.google.com/drawings/d/e/2PACX-1vQov6BGg1fXOb7Bg5zenPh7R5j6VsZJh_D6JevQ_sm_fCxmXORxad3qLIFGG1FojzJig0qdcAQoGYoN/pub?w=600&h=404)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "t5e2Eh5dGvnB",
        "colab_type": "text"
      },
      "source": [
        "And here is the code:"
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "zcEqoDfP8CP2",
        "colab_type": "code",
        "colab": {}
      },
      "source": [
        "class BasicRNN(nn.Module):\n",
        "    def __init__(self, n_inputs, n_neurons):\n",
        "        super(BasicRNN, self).__init__()\n",
        "        \n",
        "        self.Wx = torch.randn(n_inputs, n_neurons) # n_inputs X n_neurons\n",
        "        self.Wy = torch.randn(n_neurons, n_neurons) # n_neurons X n_neurons\n",
        "        \n",
        "        self.b = torch.zeros(1, n_neurons) # 1 X n_neurons\n",
        "    \n",
        "    def forward(self, X0, X1):\n",
        "        self.Y0 = torch.tanh(torch.mm(X0, self.Wx) + self.b) # batch_size X n_neurons\n",
        "        \n",
        "        self.Y1 = torch.tanh(torch.mm(self.Y0, self.Wy) +\n",
        "                            torch.mm(X1, self.Wx) + self.b) # batch_size X n_neurons\n",
        "        \n",
        "        return self.Y0, self.Y1"
      ],
      "execution_count": 0,
      "outputs": []
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "xlc4vEf88CP5",
        "colab_type": "code",
        "colab": {}
      },
      "source": [
        "N_INPUT = 3 # number of features in input\n",
        "N_NEURONS = 5 # number of units in layer\n",
        "\n",
        "X0_batch = torch.tensor([[0,1,2], [3,4,5], \n",
        "                         [6,7,8], [9,0,1]],\n",
        "                        dtype = torch.float) #t=0 => 4 X 3\n",
        "\n",
        "X1_batch = torch.tensor([[9,8,7], [0,0,0], \n",
        "                         [6,5,4], [3,2,1]],\n",
        "                        dtype = torch.float) #t=1 => 4 X 3\n",
        "\n",
        "model = BasicRNN(N_INPUT, N_NEURONS)\n",
        "\n",
        "Y0_val, Y1_val = model(X0_batch, X1_batch)"
      ],
      "execution_count": 0,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "c9VNN7jf8CP9",
        "colab_type": "text"
      },
      "source": [
        "Now when we print the outputs produced for each time step, it is of size (`4 X 5`), which represents the batch size and number of neurons, respectively."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "r1kiEzop8CP-",
        "colab_type": "code",
        "outputId": "f7c183ab-1ebe-4b05-c0bc-00a73d690168",
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 153
        }
      },
      "source": [
        "print(Y0_val)\n",
        "print(Y1_val)"
      ],
      "execution_count": 0,
      "outputs": [
        {
          "output_type": "stream",
          "text": [
            "tensor([[ 0.9975, -0.9785,  0.9822, -0.8972,  0.9929],\n",
            "        [ 0.9999, -0.9998,  1.0000, -0.9865,  0.9447],\n",
            "        [ 1.0000, -1.0000,  1.0000, -0.9983,  0.6298],\n",
            "        [-1.0000,  0.9915,  0.7409,  1.0000, -1.0000]])\n",
            "tensor([[ 0.9858, -1.0000,  1.0000, -0.8826, -1.0000],\n",
            "        [ 0.1480, -0.8635, -0.4498,  0.3516, -0.2848],\n",
            "        [-0.1455, -0.9988,  1.0000,  0.0260, -0.9997],\n",
            "        [-0.4084,  0.9973,  0.8858,  0.0783, -0.9993]])\n"
          ],
          "name": "stdout"
        }
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "BRKjsv2t8CQG",
        "colab_type": "text"
      },
      "source": [
        "### PyTorch Built-in RNN Cell"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "Rcu2H6D88CQH",
        "colab_type": "text"
      },
      "source": [
        "If you take a closer look at the `BasicRNN` computation graph we have just built, it has a serious flaw. What if we wanted to build an architecture that supports extremely large inputs and outputs. The way it is currently built, it would require us to individually compute the outputs for every time step, increasing the lines of code needed to implement the desired computation graph. Below I will show you how to consolidate and implement this more efficiently and cleanly using the built-in RNNCell module.\n",
        "\n",
        "Let's first try to implement this informally to analyze the role `RNNCell` plays:"
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "Dp0Wjh4Z8CQI",
        "colab_type": "code",
        "outputId": "2a9f4c7f-e824-4af6-fdbf-fd205e603cbb",
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 136
        }
      },
      "source": [
        "rnn = nn.RNNCell(3, 5) # n_input X n_neurons\n",
        "\n",
        "X_batch = torch.tensor([[[0,1,2], [3,4,5], \n",
        "                         [6,7,8], [9,0,1]],\n",
        "                        [[9,8,7], [0,0,0], \n",
        "                         [6,5,4], [3,2,1]]\n",
        "                       ], dtype = torch.float) # X0 and X1\n",
        "\n",
        "hx = torch.randn(4, 5) # m X n_neurons\n",
        "output = []\n",
        "\n",
        "# for each time step\n",
        "for i in range(2):\n",
        "    hx = rnn(X_batch[i], hx)\n",
        "    output.append(hx)\n",
        "\n",
        "print(output)"
      ],
      "execution_count": 0,
      "outputs": [
        {
          "output_type": "stream",
          "text": [
            "[tensor([[ 0.2545,  0.7355,  0.3708, -0.6381,  0.0402],\n",
            "        [-0.3379,  0.9996,  0.9976, -0.9769,  0.6668],\n",
            "        [-0.9940,  1.0000,  1.0000, -0.9992,  0.4488],\n",
            "        [-0.7486,  0.9925,  0.9862, -0.9642,  0.9990]], grad_fn=<TanhBackward>), tensor([[-0.9848,  1.0000,  1.0000, -0.9999,  0.9970],\n",
            "        [ 0.2496, -0.7512,  0.1730, -0.3533, -0.7347],\n",
            "        [-0.9502,  0.9998,  0.9995, -0.9966,  0.9119],\n",
            "        [-0.6488,  0.7944,  0.9580, -0.9171,  0.2384]], grad_fn=<TanhBackward>)]\n"
          ],
          "name": "stdout"
        }
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "cRNUCsXl8CQN",
        "colab_type": "text"
      },
      "source": [
        "With the above code, we have basically implemented the same model that was implemented in `BasicRNN`. `torch.RNNCell(...)` does all the magic of creating and maintaining the necessary weights and biases for us. `torch.RNNCell` accepts a tensor as input and outputs the next hidden state for each element in the batch. Read more about this module [here](https://pytorch.org/docs/stable/nn.html?highlight=rnncell#torch.nn.RNNCell).\n",
        "\n",
        "Now, let's formally build the computation graph using the same information we used above."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "Opob45Zj8CQP",
        "colab_type": "code",
        "colab": {}
      },
      "source": [
        "class CleanBasicRNN(nn.Module):\n",
        "    def __init__(self, batch_size, n_inputs, n_neurons):\n",
        "        super(CleanBasicRNN, self).__init__()\n",
        "        \n",
        "        self.rnn = nn.RNNCell(n_inputs, n_neurons)\n",
        "        self.hx = torch.randn(batch_size, n_neurons) # initialize hidden state\n",
        "        \n",
        "    def forward(self, X):\n",
        "        output = []\n",
        "\n",
        "        # for each time step\n",
        "        for i in range(2):\n",
        "            self.hx = self.rnn(X[i], self.hx)\n",
        "            output.append(self.hx)\n",
        "        \n",
        "        return output, self.hx"
      ],
      "execution_count": 0,
      "outputs": []
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "oL1yBNis8CQa",
        "colab_type": "code",
        "outputId": "2c928bbd-4abd-4454-8745-521c8e45b72b",
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 204
        }
      },
      "source": [
        "FIXED_BATCH_SIZE = 4 # our batch size is fixed for now\n",
        "N_INPUT = 3\n",
        "N_NEURONS = 5\n",
        "\n",
        "X_batch = torch.tensor([[[0,1,2], [3,4,5], \n",
        "                         [6,7,8], [9,0,1]],\n",
        "                        [[9,8,7], [0,0,0], \n",
        "                         [6,5,4], [3,2,1]]\n",
        "                       ], dtype = torch.float) # X0 and X1\n",
        "\n",
        "\n",
        "model = CleanBasicRNN(FIXED_BATCH_SIZE, N_INPUT, N_NEURONS)\n",
        "output_val, states_val = model(X_batch)\n",
        "print(output_val) # contains all output for all timesteps\n",
        "print(states_val) # contain values for final state or final timestep, i.e., t=1"
      ],
      "execution_count": 0,
      "outputs": [
        {
          "output_type": "stream",
          "text": [
            "[tensor([[ 0.4582, -0.9106,  0.0743, -0.9608,  0.9272],\n",
            "        [ 0.2087, -0.9999, -0.9486, -0.9969,  0.9996],\n",
            "        [-0.2371, -1.0000, -0.7662, -1.0000,  1.0000],\n",
            "        [-0.9576, -0.9306, -0.1201, -0.9781,  0.9277]], grad_fn=<TanhBackward>), tensor([[-0.9237, -1.0000, -0.9743, -1.0000,  1.0000],\n",
            "        [-0.3181, -0.6270, -0.6122,  0.1921,  0.0647],\n",
            "        [-0.7835, -0.9991, -0.9098, -0.9999,  0.9976],\n",
            "        [-0.5765, -0.8469, -0.5469, -0.9785,  0.7512]], grad_fn=<TanhBackward>)]\n",
            "tensor([[-0.9237, -1.0000, -0.9743, -1.0000,  1.0000],\n",
            "        [-0.3181, -0.6270, -0.6122,  0.1921,  0.0647],\n",
            "        [-0.7835, -0.9991, -0.9098, -0.9999,  0.9976],\n",
            "        [-0.5765, -0.8469, -0.5469, -0.9785,  0.7512]], grad_fn=<TanhBackward>)\n"
          ],
          "name": "stdout"
        }
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "l240cp4g8CQh",
        "colab_type": "text"
      },
      "source": [
        "You can see how the code is much cleaner since we don't need to explicitly operate on the weights as shown in the previous code snippet  --  everything is handled implicitly and eloquently behind the scenes by PyTorch."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "n3zn-Ydx8CQi",
        "colab_type": "text"
      },
      "source": [
        "### RNN for Image Classification"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "eTJHs-S-8CQk",
        "colab_type": "text"
      },
      "source": [
        "![alt txt](https://docs.google.com/drawings/d/e/2PACX-1vSiMstqkE9hTYmhPD3KMeFRNNKYA2NnrCayahBOEL1TalRqaWF7rH8a7O-nP9c-mKOdZRsWtmAGZfNN/pub?w=969&h=368)\n",
        "\n",
        "Now that you have learned how to build a simple RNN from scratch and using the built-in `RNNCell` module provided in PyTorch, let's do something more sophisticated and special.\n",
        "\n",
        "Let's try to build an image classifier using the MNIST dataset. The MNIST dataset consists of images that contain hand-written numbers from 1–10. Essentially, we want to build a classifier to predict the numbers displayed by a set of images. I know this sounds strange but you will be surprised by how well RNNs perform on this image classification task.\n",
        "\n",
        "In addition, we will also be using the `RNN` module instead of the `RNNCell` module since we want to generalize the computation graph to be able to support an `n` number of layers as well. We will only use one layer in the following computation graph, but you can experiment with the code later on by adding more layers."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "4vnhd2vR8CQo",
        "colab_type": "text"
      },
      "source": [
        "#### Importing the dataset \n",
        "Before building the RNN-based computation graph, let's import the MNIST dataset, split it into test and train portions, do a few transformations, and further explore it. You will need the following PyTorch libraries and lines of code to download and import the MNIST dataset to Google Colab."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "6FViVH8w8CQr",
        "colab_type": "code",
        "colab": {}
      },
      "source": [
        "import torchvision\n",
        "import torchvision.transforms as transforms"
      ],
      "execution_count": 0,
      "outputs": []
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "yrNdklIv8CQv",
        "colab_type": "code",
        "colab": {}
      },
      "source": [
        "%%capture\n",
        "BATCH_SIZE = 64\n",
        "\n",
        "# list all transformations\n",
        "transform = transforms.Compose(\n",
        "    [transforms.ToTensor()])\n",
        "\n",
        "# download and load training dataset\n",
        "trainset = torchvision.datasets.MNIST(root='./data', train=True,\n",
        "                                        download=True, transform=transform)\n",
        "trainloader = torch.utils.data.DataLoader(trainset, batch_size=BATCH_SIZE,\n",
        "                                          shuffle=True, num_workers=2)\n",
        "\n",
        "# download and load testing dataset\n",
        "testset = torchvision.datasets.MNIST(root='./data', train=False,\n",
        "                                       download=True, transform=transform)\n",
        "testloader = torch.utils.data.DataLoader(testset, batch_size=BATCH_SIZE,\n",
        "                                         shuffle=False, num_workers=2)"
      ],
      "execution_count": 0,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "Ixg3XnDM8CQy",
        "colab_type": "text"
      },
      "source": [
        "The code above loads and prepares the dataset to be fed into the computation graph we will build later on. Take a few minutes to play around with the code and understand what is happening. Notice that we needed to provide a batch size. This is because `trainloader` and `testloader` are iterators which will make it easier when we are iterating on the dataset and training our RNN model with minibatches."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "2f0fVaIV8CQ3",
        "colab_type": "text"
      },
      "source": [
        "#### Exloring the dataset\n",
        "Here is a few lines of code to explore the dataset. I won't cover much of what's going on here, but you can take some time and look at it by yourself."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "TEKzVTOY8CQ4",
        "colab_type": "code",
        "outputId": "542a10a8-cd20-453f-944e-983268d0d075",
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 269
        }
      },
      "source": [
        "import matplotlib.pyplot as plt\n",
        "import numpy as np\n",
        "\n",
        "# functions to show an image\n",
        "def imshow(img):\n",
        "    #img = img / 2 + 0.5     # unnormalize\n",
        "    npimg = img.numpy()\n",
        "    plt.imshow(np.transpose(npimg, (1, 2, 0)))\n",
        "\n",
        "# get some random training images\n",
        "dataiter = iter(trainloader)\n",
        "images, labels = dataiter.next()\n",
        "\n",
        "# show images\n",
        "imshow(torchvision.utils.make_grid(images))"
      ],
      "execution_count": 0,
      "outputs": [
        {
          "output_type": "display_data",
          "data": {
            "image/png": "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\nFE6ePLlatmrVqsWrV6+yoKCAPj4+NDc3f6auWFtbc+nSpczNzeXu3bs1/lCYmJiwQYMGHD16NDdv\n3szExESqVCrK5XJmZ2e/ygv5SvL+++8/t0737duXu3fvZuPGjRkeHs5Tp069jGzefBKoU6cO7ezs\n2KhRI3p6enLu3LkMDw+nQqFgYWEhN23apPFNBx73/7t27UoXF5dKj+/bt4+ZmZl0d3evdgXas2cP\nc3NzuWjRIhoZGamPubi48PDhw1y9ejUtLCx4/fp1lpaWcuvWrRqV6ebNm+ovfmlpKQsKCtS/Y2Nj\nef36dX788cfs0aOHxvfPysqKfn5+av3R0dH8/vvv6eLiwr8GgQk87jdv2LCBCoWCwcHB7NmzZ5Vt\nmZmZ8aeffqJMJuPevXtpaWlZ4bggCGzWrBnPnz/PtLQ0rl27VqPxHEEQ6OzszGXLljEkJIQqlUpN\ndHl5eTx37hwnTZqklTEOAJw0aRIvX75MZ2fnZ461bNmSfn5+vHPnDuPj4ysdc3lK3lwSMDAw4Lhx\n47hhwwbu3buXfn5+TEpKYlFRkXqQMCkpiQsXLmTDhg21cvOfJ4aGhrx16xZ/+eWXauvo0aMHHz58\nyMuXL9PQ0LDCMUtLS44fP57t2rWjnp4er1+/ToVCwaCgoGrbc3R0ZEhIiJoEcnJyeOzYMUZGRvL8\n+fMcNmxYBSLSVAYOHEipVEqpVMpr165x+PDhrFWrVoVzjIyMuH//fioUCqampvLjjz+u1sDj5MmT\nmZmZyWvXrj0zIKynp8dOnTrR19eXSUlJ/PLLL2ljY6NR2Xr16sUjR45QLpdToVBQpVKxqKiI169f\n57Jly+jo6KjV+tavXz9mZGRw27ZttLOzU5fL1dWVS5cuZX5+PrOysjh79uxqDwy+dgJ4FRLw8vJS\nN1nLWVelUjE2Npa3b99WN3Pz8vJ46NAh9unThxKJRKsPo1yGDh3KgoICTpkypdo6xowZw+TkZP7+\n++/PfLmelvKWwNGjR6ttb8SIEczIyFCTQG5uLo8fP87jx49z9uzZdHJy0tqXCwDbt2/P06dP8+jR\no+zcuTP19PQqHNfT0+OoUaOYlZXF7OxsLlu27Jn+7quIsbExfX19mZubS29v7wrHJBIJe/XqRT8/\nP4aGhnLGjBnPEG5VxcrKirdv36ZCoeDNmzd5/PhxFhQU8Pfff2enTp0oFou1Xt/Mzc35ww8/MCMj\n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\n",
            "text/plain": [
              "<Figure size 432x288 with 1 Axes>"
            ]
          },
          "metadata": {
            "tags": []
          }
        }
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "Y7NloDy08CQ9",
        "colab_type": "text"
      },
      "source": [
        "#### Model\n",
        "Let's construct the computation graph. Below are the parameters:"
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "dqjWHTo08CQ-",
        "colab_type": "code",
        "colab": {}
      },
      "source": [
        "# parameters \n",
        "N_STEPS = 28\n",
        "N_INPUTS = 28\n",
        "N_NEURONS = 150\n",
        "N_OUTPUTS = 10\n",
        "N_EPHOCS = 10"
      ],
      "execution_count": 0,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "gpFu75e88CRB",
        "colab_type": "text"
      },
      "source": [
        "And finally, here is a figure of the RNN-based classification model we are building:\n",
        "\n",
        "![alt txt](https://docs.google.com/drawings/d/e/2PACX-1vQWhELhewvq_bHgqwf4vwDb5B9DN9-jAxeTF9Y73zr-OsW6OXC-ngxAfojivXyZEhjzLXceTZU2Ncz3/pub?w=550&h=600)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "HukX7ZjP8CRC",
        "colab_type": "text"
      },
      "source": [
        "And here is the code for the model:"
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "14jNZxeg8CRD",
        "colab_type": "code",
        "colab": {}
      },
      "source": [
        "class ImageRNN(nn.Module):\n",
        "    def __init__(self, batch_size, n_steps, n_inputs, n_neurons, n_outputs):\n",
        "        super(ImageRNN, self).__init__()\n",
        "        \n",
        "        self.n_neurons = n_neurons\n",
        "        self.batch_size = batch_size\n",
        "        self.n_steps = n_steps\n",
        "        self.n_inputs = n_inputs\n",
        "        self.n_outputs = n_outputs\n",
        "        \n",
        "        self.basic_rnn = nn.RNN(self.n_inputs, self.n_neurons) \n",
        "        \n",
        "        self.FC = nn.Linear(self.n_neurons, self.n_outputs)\n",
        "        \n",
        "    def init_hidden(self,):\n",
        "        # (num_layers, batch_size, n_neurons)\n",
        "        return (torch.zeros(1, self.batch_size, self.n_neurons))\n",
        "        \n",
        "    def forward(self, X):\n",
        "        # transforms X to dimensions: n_steps X batch_size X n_inputs\n",
        "        X = X.permute(1, 0, 2) \n",
        "        \n",
        "        self.batch_size = X.size(1)\n",
        "        self.hidden = self.init_hidden()\n",
        "        \n",
        "        # lstm_out => n_steps, batch_size, n_neurons (hidden states for each time step)\n",
        "        # self.hidden => 1, batch_size, n_neurons (final state from each lstm_out)\n",
        "        lstm_out, self.hidden = self.basic_rnn(X, self.hidden)      \n",
        "        out = self.FC(self.hidden)\n",
        "        \n",
        "        return out.view(-1, self.n_outputs) # batch_size X n_output"
      ],
      "execution_count": 0,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "8Yw8fFlp8CRJ",
        "colab_type": "text"
      },
      "source": [
        "The `ImageRNN` model is doing the following:\n",
        "- The initialization function `__init__(...)` declares a few variables, and then a basic RNN layer `basic_rnn` followed by a fully-connected layer `self.FC`.\n",
        "- The `init_hidden` function initializes hidden weights with zero values. \n",
        "The `forward` function accepts an input of size `n_steps X batch_size X n_neurons`. Then the data flows through the RNN layer and then through the fully-connected layer. \n",
        "- The output are the log probabilities of the model."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "bJUCKm_U8CRK",
        "colab_type": "text"
      },
      "source": [
        "#### Testing the model with some samples\n",
        "A very good practice encouraged by PyTorch developers throughout their documentation, and which I really like and highly recommend, is to always test the model with a portion of the dataset before actual training. This is to ensure that you have the correct dimension specified and that the model is outputing the information you expect. Below I show an example of how to test your model:"
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "P6W0jwK08CRM",
        "colab_type": "code",
        "outputId": "137bf966-2157-470a-edde-de663bec8f08",
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 357
        }
      },
      "source": [
        "dataiter = iter(trainloader)\n",
        "images, labels = dataiter.next()\n",
        "model = ImageRNN(BATCH_SIZE, N_STEPS, N_INPUTS, N_NEURONS, N_OUTPUTS)\n",
        "logits = model(images.view(-1, 28,28))\n",
        "print(logits[0:10])"
      ],
      "execution_count": 0,
      "outputs": [
        {
          "output_type": "stream",
          "text": [
            "tensor([[-0.0937, -0.0978, -0.0586,  0.0161,  0.0557,  0.0227, -0.0226, -0.0067,\n",
            "          0.1092, -0.1295],\n",
            "        [-0.0878, -0.0855, -0.0318,  0.0267,  0.0569,  0.0349, -0.0275,  0.0007,\n",
            "          0.0999, -0.1215],\n",
            "        [-0.0829, -0.1012, -0.0541,  0.0155,  0.0562,  0.0162, -0.0258, -0.0100,\n",
            "          0.1077, -0.1310],\n",
            "        [-0.1004, -0.0744, -0.0163,  0.0465,  0.0382,  0.0289, -0.0569,  0.0015,\n",
            "          0.1003, -0.1266],\n",
            "        [-0.0946, -0.0994, -0.0636,  0.0132,  0.0539,  0.0236, -0.0221, -0.0034,\n",
            "          0.1013, -0.1298],\n",
            "        [-0.0922, -0.0974, -0.0334,  0.0369,  0.0622,  0.0378, -0.0497,  0.0005,\n",
            "          0.0983, -0.1160],\n",
            "        [-0.0834, -0.0942, -0.0414,  0.0258,  0.0573,  0.0174, -0.0218, -0.0105,\n",
            "          0.1045, -0.1307],\n",
            "        [-0.0782, -0.0985, -0.0458,  0.0154,  0.0579,  0.0214, -0.0227, -0.0060,\n",
            "          0.1035, -0.1269],\n",
            "        [-0.1019, -0.0963, -0.0549,  0.0214,  0.0551,  0.0203, -0.0167, -0.0048,\n",
            "          0.1131, -0.1316],\n",
            "        [-0.1078, -0.1001, -0.0372,  0.0187,  0.0682,  0.0412, -0.0265, -0.0021,\n",
            "          0.1033, -0.1191]], grad_fn=<SliceBackward>)\n"
          ],
          "name": "stdout"
        }
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "BKDkckp98CRQ",
        "colab_type": "text"
      },
      "source": [
        "#### Training"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "46Qke-c08CRR",
        "colab_type": "text"
      },
      "source": [
        "Now let's look at the code for training the image classification model. But first, let's declare a few helper functions needed to train the model:"
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "BoDWoBuy8CRS",
        "colab_type": "code",
        "colab": {}
      },
      "source": [
        "import torch.optim as optim\n",
        "\n",
        "# Device\n",
        "device = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\n",
        "\n",
        "# Model instance\n",
        "model = ImageRNN(BATCH_SIZE, N_STEPS, N_INPUTS, N_NEURONS, N_OUTPUTS)\n",
        "criterion = nn.CrossEntropyLoss()\n",
        "optimizer = optim.Adam(model.parameters(), lr=0.001)\n",
        "\n",
        "def get_accuracy(logit, target, batch_size):\n",
        "    ''' Obtain accuracy for training round '''\n",
        "    corrects = (torch.max(logit, 1)[1].view(target.size()).data == target.data).sum()\n",
        "    accuracy = 100.0 * corrects/batch_size\n",
        "    return accuracy.item()"
      ],
      "execution_count": 0,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "uvCzj28o8CRX",
        "colab_type": "text"
      },
      "source": [
        "Before training a model in PyTorch, you can programatically specify what device you want to use during training; the `torch.device(...)` function tells the program that we want to use the GPU if one is available, otherwise the CPU will be the default device.\n",
        "\n",
        "Then we create an instance of the model, `ImageRNN(...)``, with the proper parameters. The criterion represents the function we will use to compute the loss of the model. The `nn.CrossEntropyLoss()` function basically applies a log softmax followed by a negative log likelihood loss operation over the output of the model. To compute the loss, the function needs both the log probabilities and targets. We will see later in our code how to provide this to the criterion.\n",
        "\n",
        "For training, we also need an optimization algorithm which helps to update weights based on the current loss. This is achieved with the `optim.Adam` optimization function, which requires the model parameters and a learning rate. Alternatively, you can also use `optim.SGD` or any other optimization algorithm that's available. \n",
        "\n",
        "The `get_accuracy(...)` function simply computes the accuracy of the model given the log probabilities and target values. As an exercise, you can write code to test this function as we did with the model before.\n",
        "\n",
        "Let's put everything together and train our image classification model:"
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "LaAmGhZT8CRX",
        "colab_type": "code",
        "outputId": "683f9327-9ae2-400e-f25f-3a567f71fad6",
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 187
        }
      },
      "source": [
        "for epoch in range(N_EPHOCS):  # loop over the dataset multiple times\n",
        "    train_running_loss = 0.0\n",
        "    train_acc = 0.0\n",
        "    model.train()\n",
        "    \n",
        "    # TRAINING ROUND\n",
        "    for i, data in enumerate(trainloader):\n",
        "         # zero the parameter gradients\n",
        "        optimizer.zero_grad()\n",
        "        \n",
        "        # reset hidden states\n",
        "        model.hidden = model.init_hidden() \n",
        "        \n",
        "        # get the inputs\n",
        "        inputs, labels = data\n",
        "        inputs = inputs.view(-1, 28,28) \n",
        "\n",
        "        # forward + backward + optimize\n",
        "        outputs = model(inputs)\n",
        "\n",
        "        loss = criterion(outputs, labels)\n",
        "        loss.backward()\n",
        "        optimizer.step()\n",
        "\n",
        "        train_running_loss += loss.detach().item()\n",
        "        train_acc += get_accuracy(outputs, labels, BATCH_SIZE)\n",
        "         \n",
        "    model.eval()\n",
        "    print('Epoch:  %d | Loss: %.4f | Train Accuracy: %.2f' \n",
        "          %(epoch, train_running_loss / i, train_acc/i))"
      ],
      "execution_count": 0,
      "outputs": [
        {
          "output_type": "stream",
          "text": [
            "Epoch:  0 | Loss: 0.7489 | Train Accuracy: 75.88\n",
            "Epoch:  1 | Loss: 0.3113 | Train Accuracy: 91.05\n",
            "Epoch:  2 | Loss: 0.2325 | Train Accuracy: 93.33\n",
            "Epoch:  3 | Loss: 0.1957 | Train Accuracy: 94.53\n",
            "Epoch:  4 | Loss: 0.1706 | Train Accuracy: 95.21\n",
            "Epoch:  5 | Loss: 0.1564 | Train Accuracy: 95.58\n",
            "Epoch:  6 | Loss: 0.1471 | Train Accuracy: 95.91\n",
            "Epoch:  7 | Loss: 0.1329 | Train Accuracy: 96.14\n",
            "Epoch:  8 | Loss: 0.1283 | Train Accuracy: 96.42\n",
            "Epoch:  9 | Loss: 0.1196 | Train Accuracy: 96.65\n"
          ],
          "name": "stdout"
        }
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "93-DMqi58CRf",
        "colab_type": "text"
      },
      "source": [
        "We can also compute accuracy on the testing dataset to test how well the model performs on the image classification task. As you can see below, our RNN model is performing very well on the MNIST classification task.\n"
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "BzeqYT4N8CRi",
        "colab_type": "code",
        "outputId": "f6b6cf03-8a59-40cf-e741-b8130859a26f",
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 34
        }
      },
      "source": [
        "test_acc = 0.0\n",
        "for i, data in enumerate(testloader, 0):\n",
        "    inputs, labels = data\n",
        "    inputs = inputs.view(-1, 28, 28)\n",
        "\n",
        "    outputs = model(inputs)\n",
        "\n",
        "    test_acc += get_accuracy(outputs, labels, BATCH_SIZE)\n",
        "        \n",
        "print('Test Accuracy: %.2f'%( test_acc/i))"
      ],
      "execution_count": 0,
      "outputs": [
        {
          "output_type": "stream",
          "text": [
            "Test Accuracy: 95.83\n"
          ],
          "name": "stdout"
        }
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "FD98PsQi8CRv",
        "colab_type": "text"
      },
      "source": [
        "### Final Words\n",
        "Please notice that we are not using GPU in this tutorial since the models we are building are relatively simple. As an exercise, you can take a look at the [PyTorch documentation](https://pytorch.org/docs/stable/notes/cuda.html) to learn how to program specific operations to execute on the GPU. You can then try to optimize the code to run on the GPU. If you need help with this, reach out to me on [Twitter](https://twitter.com/omarsar0).\n",
        "\n",
        "That's it for this tutorial. Congratulations! You are now able to implement a basic RNN in PyTorch. You also learned how to apply RNNs to solve a real-world, image classification problem.\n",
        "\n",
        "In the next tutorial, we will do more advanced things with RNNs and try to solve even more complex problems, such as sarcasm detection and sentiment classification. Until next time!"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "jKAm1xB08CRx",
        "colab_type": "text"
      },
      "source": [
        "### References\n",
        "- [A Simple Neural Network from Scratch with PyTorch and Google Colab](https://github.com/omarsar/pytorch_intro_neural_network/blob/master/nn.ipynb)\n",
        "- [Hands on Machine Learning with Scikit-learn and Tensorflow](http://shop.oreilly.com/product/0636920052289.do)"
      ]
    }
  ]
}